package ast

import (
	"fmt"
	"io"
	"os"
	"reflect"
)

// Print prints x to standard output, skipping nil fields.
// Print(fset, x) is the same as Fprint(os.Stdout, fset, x, NotNilFilter).
func Print(node Node, printDoc bool) (err error) {
	// setup printer
	p := printer{
		printDoc: printDoc,
		output: os.Stdout,
		last:   '\n', // force printing of line number on first line
	}

	// install error handler
	defer func() {
		if e := recover(); e != nil {
			err = e.(localError).err // re-panics if it's not a localError
		}
	}()

	p.printNode("Root", node)
	p.printf("\n")

	return
}

type printer struct {
	printDoc bool
	output   io.Writer
	file     *SourceFile
	indent   int  // current indentation level
	last     byte // the last byte processed by Write
	line     int  // current line number
}

var indent = []byte(".  ")

func (p *printer) Write(data []byte) (n int, err error) {
	var m int
	for i, b := range data {
		// invariant: data[0:n] has been written
		if b == '\n' {
			m, err = p.output.Write(data[n : i+1])
			n += m
			if err != nil {
				return
			}
			p.line++
		} else if p.last == '\n' {
			_, err = fmt.Fprintf(p.output, "%6d  ", p.line)
			if err != nil {
				return
			}
			for j := p.indent; j > 0; j-- {
				_, err = p.output.Write(indent)
				if err != nil {
					return
				}
			}
		}
		p.last = b
	}
	if len(data) > n {
		m, err = p.output.Write(data[n:])
		n += m
	}
	return
}

// localError wraps locally caught errors so we can distinguish
// them from genuine panics which we don't want to return as errors.
type localError struct {
	err error
}

// printf is a convenience wrapper that takes care of print errors.
func (p *printer) printf(format string, args ...interface{}) {
	if _, err := fmt.Fprintf(p, format, args...); err != nil {
		panic(localError{err})
	}
}

func (p *printer) scope(f func()) {
	p.indent ++
	f()
	p.indent--
}

func isNil(i interface{}) bool {
	ret := i == nil

	if !ret {
		vi := reflect.ValueOf(i)
		kind := reflect.ValueOf(i).Kind()
		if kind == reflect.Slice ||
			kind == reflect.Map ||
			kind == reflect.Chan ||
			kind == reflect.Interface ||
			kind == reflect.Func ||
			kind == reflect.Ptr {
			return vi.IsNil()
		}
	}

	return ret
}

func (p *printer) printDocuments(node Node) {
	var documents = node.GetDocuments()
	if documents != nil {
		p.scope(func() {
			p.printf("Documents: [ \n")
			p.scope(func() {
				for i, doc := range documents {
					p.printNode(fmt.Sprintf("[%d]", i), doc)
				}
			})
			p.printf("]\n")
		})
	}
}

func (p *printer) printNode(prefix string, node Node) {
	if isNil(node) {
		p.printf("%s: nil\n", prefix)
		return
	}

	v := reflect.ValueOf(node)
	p.printf("%s: %s %d:%d { \n", prefix, v.Type(), node.Pos(), node.End())

	if p.printDoc {
		p.printDocuments(node)
	}

	switch n := node.(type) {
	case *TokenNode:
		p.scope(func() {
			p.printf("Token: %q\n", n.Token.ToString())
		})

	case *Modifier:
		p.scope(func() {
			p.printf("Token: %q\n", n.Token.ToString())
		})

	case *Annotation:
		p.scope(func() {
			p.printNode("Type", n.Type)
			p.printList("Arguments", n.Arguments)
		})

	case *QualifiedName:
		p.scope(func() {
			p.printNode("Left", n.Left)
			p.printNode("Right", n.Right)
		})

	case *HeritageClause:
		p.scope(func() {
			p.printList("Types", n.Types)
		})

	case *CaseClause:
		p.scope(func() {
			p.printNode("Token", n.Token)
			p.printNode("Cases", n.Expression)
			p.printList("Body", n.Statements)
		})

	case *CatchClause:
		p.scope(func() {
			p.printNode("VariableDeclaration", n.VariableDeclaration)
			p.printNode("Block", n.Block)
		})

	case *CaseBlock:
		p.scope(func() {
			p.printList("Clauses", n.Clauses)
		})

	case *BadDeclaration:

	case *PackageDeclaration:
		p.scope(func() {
			p.printNode("Name", n.Name)
		})

	case *ImportDeclaration:
		p.scope(func() {
			p.printNode("Path", n.Path)
			p.printNode("Target", n.Target)
		})

	case *VariableDeclaration:
		p.scope(func() {
			p.printNode("Name", n.Name)
			p.printNode("Type", n.Type)
			p.printNode("Initializer", n.Initializer)
		})

	case *DocumentTypeDeclaration:
		p.scope(func() {
			p.printNode("Dots", n.Dots)
			p.printNode("Type", n.Type)
		})

	case *ParameterDeclaration:
		p.scope(func() {
			p.printNode("Name", n.Name)
			p.printNode("Type", n.Type)
			p.printNode("Initializer", n.Initializer)
			p.printNode("Dots", n.Dots)
		})

	case *TypeParameterDeclaration:
		p.scope(func() {
			p.printNode("Name", n.Name)
			p.printNode("Constraint", n.Constraint)
		})
		
	case *ConstructorDeclaration:
		p.scope(func() {
			p.printList("TypeParameters", n.TypeParameters)
			p.printList("Parameters", n.Parameters)
			p.printNode("Type", n.Type)
			p.printList("Throws", n.Throws)
			p.printNode("Body", n.Body)
		})

	case *FieldDeclaration:
		p.scope(func() {
			p.printNode("Name", n.Name)
			p.printNode("Type", n.Type)
			p.printNode("Initializer", n.Initializer)
		})

	case *MethodDeclaration:
		p.scope(func() {
			p.printNode("Name", n.Name)
			p.printList("TypeParameters", n.TypeParameters)
			p.printList("Parameters", n.Parameters)
			p.printNode("Type", n.Type)
			p.printList("Throws", n.Throws)
			p.printNode("Default", n.Default)
			p.printNode("Value", n.Value)
			p.printNode("Body", n.Body)
		})

	case *ClassDeclaration:
		p.scope(func() {
			p.printNode("Name", n.Name)
			p.printList("TypeParameters", n.TypeParameters)
			p.printList("HeritageClauses", n.HeritageClauses)
			p.printList("Members", n.Members)
		})

	case *BodyDeclaration:
		p.scope(func() {
			p.printList("Members", n.Members)
		})

	case *EnumItemDeclaration:
		p.scope(func() {
			p.printNode("Name", n.Name)
			p.printList("Arguments", n.Arguments)
		})

	case *EnumDeclaration:
		p.scope(func() {
			p.printNode("Name", n.Name)
			p.printList("TypeParameters", n.TypeParameters)
			p.printList("HeritageClauses", n.HeritageClauses)
			p.printList("Enums", n.EnumItems)
			p.printList("Members", n.Members)
		})

	case *InterfaceDeclaration:
		p.scope(func() {
			p.printNode("Name", n.Name)
			p.printList("TypeParameters", n.TypeParameters)
			p.printList("HeritageClauses", n.HeritageClauses)
			p.printList("Members", n.Members)
		})

	case *AnnotationDeclaration:
		p.scope(func() {
			p.printNode("Name", n.Name)
			p.printList("TypeParameters", n.TypeParameters)
			p.printList("HeritageClauses", n.HeritageClauses)
			p.printList("Members", n.Members)
		})

	case *SourceFile:
		p.scope(func() {
			p.printNode("PackageDeclaration", n.PackageDeclaration)
			p.printList("Imports", n.Imports)
			p.printList("Declarations", n.Declarations)
			p.printNode("EndOfFileToken", n.EndOfFileToken)
		})

	case *OmittedExpression:
		// Nothing to do

	case *Identifier:
		p.scope(func() {
			p.printf("Value: %q\n", n.Value)
			p.printf("OriginalToken: %s\n", n.OriginalToken.ToString())
		})

	case *ArrowExpression:
		p.scope(func() {
			p.printNode("Declaration", n.Declaration)
		})

	case *PrefixUnaryExpression:
		p.scope(func() {
			p.printNode("Operator", n.Operator)
			p.printNode("Operand", n.Operand)
		})

	case *PostfixUnaryExpression:
		p.scope(func() {
			p.printNode("Operand", n.Operand)
			p.printNode("Operator", n.Operator)
		})

	case *BinaryExpression:
		p.scope(func() {
			p.printNode("Left", n.Left)
			p.printNode("Operator", n.Operator)
			p.printNode("Right", n.Right)
		})

	case *ConditionalExpression:
		p.scope(func() {
			p.printNode("Condition", n.Condition)
			p.printNode("QuestionTok", n.QuestionTok)
			p.printNode("WhenTrue", n.WhenTrue)
			p.printNode("ColonTok", n.ColonTok)
			p.printNode("WhenFalse", n.WhenFalse)
		})

	case *ThisExpression:
		p.scope(func() {
			p.printNode("X", n.Expression)
		})

	case *SuperExpression:
		p.scope(func() {
			p.printNode("X", n.Expression)
		})

	case *ArrowFunctionDeclaration:
		p.scope(func() {
			p.printList("TypeParameters", n.TypeParameters)
			p.printList("Parameters", n.Parameters)
			p.printNode("Type", n.Type)
			p.printList("Throws", n.Throws)
			p.printNode("Arrow", n.Arrow)
			p.printNode("Body", n.Body)
		})

	case *QualifiedExpression:
		p.scope(func() {
			p.printNode("Type", n.Type)
		})

	case *ConstructorExpression:
		p.scope(func() {
			p.printNode("Caller", n.Scope)
			p.printNode("TypeName", n.TypeName)
			p.printList("TypeArguments", n.TypeArguments)
			p.printList("Arguments", n.Arguments)
		})

	case *NewObjectExpression:
		p.scope(func() {
			p.printNode("Call", n.Type)
			p.printNode("Body", n.Body)
		})

	case *NewArrayExpression:
		p.scope(func() {
			p.printNode("Type", n.Type)
			p.printNode("Initializer", n.Initializer)
		})

	case *AsExpression:
		p.scope(func() {
			p.printNode("Expression", n.Expression)
			p.printNode("Type", n.Type)
		})

	case *ArrayLiteralExpression:
		p.scope(func() {
			p.printList("Elements", n.Elements)
		})

	case *ParenthesizedExpression:
		p.scope(func() {
			p.printNode("Expression", n.Expression)
		})

	case *ElementAccessExpression:
		p.scope(func() {
			p.printNode("Expression", n.Expression)
			p.printNode("ArgumentExpression", n.ArgumentExpression)
		})

	case *LiteralExpression:
		p.scope(func() {
			p.printf("Value: %q\n", n.Value)
		})

	case *ClassExpression:
		p.scope(func() {
			p.printNode("Type", n.Type)
		})

	case *InstanceAccessExpression:
		p.scope(func() {
			p.printNode("Left", n.Left)
			p.printNode("Right", n.Right)
		})

	case *SelectorExpression:
		p.scope(func() {
			p.printNode("Expression", n.Expression)
			p.printNode("Name", n.Name)
		})

	case *CallExpression:
		p.scope(func() {
			p.printNode("Expression", n.Expression)
			p.printList("TypeArguments", n.TypeArguments)
			p.printList("Arguments", n.Arguments)
		})

	case *DocumentCallExpression:
		p.scope(func() {
			p.printNode("Expression", n.Expression)
			p.printList("Arguments", n.Arguments)
		})

	case *EmptyStatement:
		// Nothing to do

	case *DeclarationStatement:
		p.scope(func() {
			p.printNode("Declaration", n.Declaration)
		})

	case *ExpressionStatement:
		p.scope(func() {
			p.printNode("Expression", n.Expression)
		})

	case *BlockStatement:
		p.scope(func() {
			p.printList("Statements", n.Statements)
		})

	case *TryStatement:
		p.scope(func() {
			p.printNode("TryBlock", n.TryBlock)
			p.printList("CatchClauses", n.CatchClauses)
			p.printNode("FinallyBlock", n.FinallyBlock)
		})

	case *IfStatement:
		p.scope(func() {
			p.printNode("Condition", n.Condition)
			p.printNode("ThenStatement", n.ThenStatement)
			p.printNode("ElseStatement", n.ElseStatement)
		})

	case *DoStatement:
		p.scope(func() {
			p.printNode("Body", n.Body)
			p.printNode("Condition", n.Condition)
		})

	case *WhileStatement:
		p.scope(func() {
			p.printNode("Condition", n.Condition)
			p.printNode("Body", n.Body)
		})

	case *ForStatement:
		p.scope(func() {
			p.printNode("Initializer", n.Initializer)
			p.printNode("Condition", n.Condition)
			p.printNode("Incrementor", n.Incrementor)
			p.printNode("Body", n.Body)
		})

	case *ForOfStatement:
		p.scope(func() {
			p.printNode("VariableDeclaration", n.VariableDeclaration)
			p.printNode("Expression", n.Expression)
			p.printNode("Body", n.Body)
		})

	case *BranchStatement:
		p.scope(func() {
			p.printNode("Label", n.Label)
		})

	case *ThrowStatement:
		p.scope(func() {
			p.printNode("Expression", n.Expression)
		})

	case *ReturnStatement:
		p.scope(func() {
			p.printNode("Expression", n.Expression)
		})

	case *SwitchStatement:
		p.scope(func() {
			p.printNode("Expression", n.Expression)
			p.printNode("Body", n.Body)
		})

	case *LabeledStatement:
		p.scope(func() {
			p.printNode("Label", n.Label)
			p.printNode("Stmt", n.Statement)
		})

	case *BoundsType:
		p.scope(func() {
			p.printNode("Bound", n.Bound)
			p.printNode("Type", n.Type)
		})

	case *IntrinsicType:
		p.scope(func() {
			p.printf("Token: %q\n", n.Token.ToString())
		})

	case *TypeReference:
		p.scope(func() {
			p.printNode("Name", n.Name)
			p.printList("TypeArgs", n.TypeArguments)
		})

	case *ArrayType:
		p.scope(func() {
			p.printNode("Ele", n.Element)
			p.printNode("Dimension", n.Dimension)
		})

	case *UnionType:
		p.scope(func() {
			p.printList("Types", n.Types)
		})

	case *IntersectionType:
		p.scope(func() {
			p.printList("Types", n.Types)
		})

	case *DocRoot:
		p.scope(func() {
			p.printList("Children", n.Children)
		})

	case *Number:
		p.scope(func() {
			p.printf("Value: %q\n", n.Value)
		})

	case *Version:
		p.scope(func() {
			p.printNode("Left", n.Left)
			p.printNode("Right", n.Right)
		})

	case *PlainText:
		p.scope(func() {
			p.printf("Value: %q\n", n.Value)
		})

	case *Header:
		p.scope(func() {
			p.printf("Token: %q\n", n.Token.ToString())
			p.printNode("Text:", n.Text)
		})

	case *Bold:
		p.scope(func() {
			p.printNode("Text:", n.Text)
		})

	case *Italic:
		p.scope(func() {
			p.printNode("Text:", n.Text)
		})

	case *BoldItalic:
		p.scope(func() {
			p.printNode("Text:", n.Text)
		})

	case *Paragraph:
		p.scope(func() {
			p.printList("Children", n.Content)
		})

	case *CodeText:
		p.scope(func() {
			p.printNode("Text:", n.Content)
		})

	case *CodeBlock:
		p.scope(func() {
			p.printNode("Language:", n.Language)
			p.printList("Text:", n.Content)
		})

	case *CodeLink:
		p.scope(func() {
			p.printNode("Name", n.Name)
			p.printNode("Expression", n.Expression)
		})

	case *Link:
		p.scope(func() {
			p.printNode("Name", n.Name)
			p.printNode("Address", n.Address)
		})

	case *LineBreak:
		// Nothing to do

	case *OrderedList:
		p.scope(func() {
			p.printf("Level: %d\n", n.Level)
			p.printList("Content", n.Content)
		})

	case *UnorderedList:
		p.scope(func() {
			p.printf("Level: %d\n", n.Level)
			p.printList("Content", n.Content)
		})

	case *ListItem:
		p.scope(func() {
			p.printNode("Content", n.Content)
		})

	case *AuthorTag:
		p.scope(func() {
			p.printNode("Content", n.Content)
		})

	case *AliasTag:
		p.scope(func() {
			p.printNode("Name", n.Name)
			p.printNode("Content", n.Content)
		})

	case *ParameterTag:
		p.scope(func() {
			p.printf("IsTypeParameter: %v\n", n.IsTypeParameter)
			p.printNode("Name", n.Name)
			p.printNode("Content", n.Content)
		})


	case *ReturnTag:
		p.scope(func() {
			p.printNode("Content", n.Content)
		})

	case *ExceptionTag:
		p.scope(func() {
			p.printNode("Type", n.Type)
			p.printNode("Content", n.Content)
		})

	case *SeeTag:
		p.scope(func() {
			p.printNode("Expression", n.Expression)
			p.printNode("Content", n.Content)
		})

	case *SinceTag:
		p.scope(func() {
			p.printNode("Version", n.Version)
			p.printNode("Content", n.Content)
		})

	case *VersionTag:
		p.scope(func() {
			p.printNode("Version", n.Version)
			p.printNode("Content", n.Content)
		})

	case *DeprecatedTag:
		p.scope(func() {
			p.printNode("Content", n.Content)
		})



	default:
		panic(fmt.Sprintf("ast.Walk: unexpected node type %T", n))
	}

	p.printf("}\n")
}

func (p *printer) printList(prefix string, list List) {
	if isNil(list) {
		p.printf("%s: nil\n", prefix)
		return
	}

	v := reflect.ValueOf(list)
	p.printf("%s: %s %d:%d [ \n", prefix, v.Type(), list.Pos(), list.End())
	p.scope(func() {
		for i := 0; i < list.Len(); i++ {
			p.printNode(fmt.Sprintf("[%d]", i), list.NodeAt(i))
		}
	})
	p.printf("]\n")
}
